#include <netdb.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include "psd_conn_udp.h"
#include "psd_protocols.h"
#include "psd_logger.h"
#include "psd_config.h"
#include "psd_tpool.h"
#include "psd_modules.h"
#include "psd_settings.h"
#include "psd_listener.h"

#include "compat.h"

struct udp_listener_args {
	int socket;
	pthread_t accept_thread;
};

int psd_proto_udp_init();
static psdConn *psd_proto_udp_connect(const char *depot_id, psdSettings *settings);
static psdListener *psd_proto_udp_setup_listener(const char *listener_id, psdSettings *settings, int one_shot, listener_cb callback, void *arg);
static int psd_proto_udp_start_listener(psdListener *listener);
static int psd_proto_udp_stop_listener(psdListener *listener);
static void psd_proto_udp_free_listener(psdListener *listener);
static void *psd_udp_accept_handler(void *v);

static psdProtocolHandler psd_udp_handler = {
	.connect = psd_proto_udp_connect,
	.setup_listener = psd_proto_udp_setup_listener,
	.name = "udp"
};

static psdModule psd_udp_module = {
	.desc = "UDP Handler Module",
	.dependencies = "udp", 			// XXX: gross hack
	.init = psd_proto_udp_init
};

psdModule *module_info() {
	return &psd_udp_module;
}

int udp_def_use_ipv6 = 0;
int udp_def_incoming_port = 5006;
int udp_def_outgoing_port = 5006;
int udp_def_incoming_recv_bufsize = 0;
int udp_def_incoming_send_bufsize = 0;
int udp_def_outgoing_recv_bufsize = 0;
int udp_def_outgoing_send_bufsize = 0;

int psd_proto_udp_init() {

	if (psd_add_protocol_handler(&psd_udp_handler)) {
		psd_err(0, "couldn't add protocol handler");
		goto error_exit;
	}

	return 0;
	
error_exit:
	return -1;
}

static psdConn *psd_proto_udp_connect(const char *hostname, psdSettings *settings) {
	struct addrinfo hints;
	struct addrinfo *depot_addrs, *nexthop;
	char port_str[10];
	int port;
	int retval;
	psdConn *ret_conn;
	int connected;
	int new_sd = 0;
	int recv_bufsize;
	int send_bufsize;
	int use_ipv6;
	char *bind_interface = NULL;

	struct hostent *he;
	struct sockaddr_storage sa;

	// grab the udp settings from the given policy
	if (psd_settings_get_int_2(settings, "udp", "port", &port) != 0) {
		port = udp_def_outgoing_port;
	}

	if (psd_settings_get_int_2(settings, "udp", "recv_bufsize", &recv_bufsize) != 0) {
		recv_bufsize = udp_def_outgoing_recv_bufsize;
	}

	if (psd_settings_get_int_2(settings, "udp", "send_bufsize", &send_bufsize) != 0) {
		send_bufsize = udp_def_outgoing_send_bufsize;
	}

	if (psd_settings_get_bool_2(settings, "udp", "use_ipv6", &use_ipv6) != 0) {
		use_ipv6 = udp_def_use_ipv6;
	}

	psd_info(1, "setting up UDP socket structure for %s/%d", hostname, port);

	snprintf(port_str, 10, "%d", port);

	bzero(&hints, sizeof(struct addrinfo));

	if (use_ipv6) {
		hints.ai_family = AF_INET6;
	} else {
		hints.ai_family = AF_INET;
	}

	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;
	hints.ai_flags |= AI_PASSIVE;

	// we'll deal with proper binding and hostname lookups later
	/*	
	if (bind_interface != NULL) {
                he = gethostbyname((const char *) bind_interface);
                if (he != NULL) {
                        bzero((void *)&sa, sizeof(struct sockaddr_storage));
                        ((struct sockaddr *)&sa)->sa_family = he->h_addrtype;
                        if (he->h_addrtype == AF_INET) {
                                memcpy (&(((struct sockaddr_in *) &sa)->sin_addr), he->h_addr_list[0], he->h_length);
                                ((struct sockaddr_in *) &sa)->sin_port = 0;
                        } else {
                                memcpy (&(((struct sockaddr_in6 *) &sa)->sin6_addr), he->h_addr_list[0], he->h_length);
                                ((struct sockaddr_in6 *) &sa)->sin6_port = 0;
                        }
                } else {
                        bind_interface = NULL;
                }
        }

	retval = getaddrinfo(hostname, port_str, &hints, &gw_addrs);
	if (retval != 0) {
		psd_err(1, "lookup of \"%s\" failed", hostname);
		goto error_exit;
	}

	for(nexthop = gw_addrs; nexthop != NULL && !connected; nexthop = nexthop->ai_next) {
		new_sd = socket(nexthop->ai_family, nexthop->ai_socktype, nexthop->ai_protocol);
		if (new_sd < 0) {
			psd_warn(10, "socket() failed");
			continue;
		}

		if (bind_interface != NULL) {
			bind(new_sd, (struct sockaddr *)&sa, sizeof(sa));
		}

		if (recv_bufsize > 0) {
			setsockopt(new_sd, SOL_SOCKET, SO_RCVBUF, (char *)&recv_bufsize, sizeof(int));
		}

		if (send_bufsize > 0) {
			setsockopt(new_sd, SOL_SOCKET, SO_SNDBUF, (char *)&send_bufsize, sizeof(int));
		}
	}

	freeaddrinfo(gw_addrs);

	*/

	new_sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (new_sd < 0) {
		psd_err(0, "socket() failed");
		goto error_exit;
	}

	struct sockaddr_in si_other;

	memset((char *) &si_other, 0, sizeof(si_other));
	si_other.sin_family = AF_INET;
	si_other.sin_port = htons(port);

	if (inet_aton(hostname, &si_other.sin_addr) == 0) {
		psd_err(0, "inet_aton() failed\n");
		
	}

	ret_conn = psd_conn_udp_alloc(new_sd);
	if (!ret_conn) {
		psd_err(10, "couldn't allocate socket structure");
		goto error_exit_sd;
	}
	
	psdConn_udpData *udp_data = (psdConn_udpData *) ret_conn->conn_private;
	if (!udp_data) {
		psd_err(0, "could not get udp_data");
		goto error_exit_sd;
	}
	
	memcpy(&(udp_data->si_other), &si_other, sizeof(si_other));
	udp_data->slen = sizeof(si_other);

	psd_info(1, "UDP conn ready to send to %s", hostname);

	return ret_conn;

error_exit_sd:
	close(new_sd);
error_exit:
	return NULL;
}

static psdListener *psd_proto_udp_setup_listener(const char *listener_id, psdSettings *settings, int one_shot, listener_cb callback, void *arg) {
	psdListener *new_listener;
	struct udp_listener_args *udp_args;

	new_listener = psd_listener_alloc();
	if (!new_listener)
		goto error_exit;

	new_listener->id = strdup(listener_id);
	if (!new_listener->id)
		goto error_exit_listener;

	new_listener->settings = psd_settings_duplicate(settings);
	if (!new_listener->settings)
		goto error_exit_listener;

	udp_args = malloc(sizeof(struct udp_listener_args));
	if (!udp_args)
		goto error_exit_listener;

	new_listener->status = LISTENER_STOPPED;
	new_listener->callback = callback;
	new_listener->arg = arg;
	new_listener->protocol = "UDP";
	new_listener->proto_private = udp_args;
	new_listener->start = psd_proto_udp_start_listener;
	new_listener->stop = psd_proto_udp_stop_listener;
	new_listener->free = psd_proto_udp_free_listener;

	return new_listener;

error_exit_listener:
	psd_listener_free(new_listener);
error_exit:
	return NULL;
}

static int psd_proto_udp_start_listener(psdListener *listener) {
	struct addrinfo *ai;
	int i;
	int family;
	int use_ipv6;
	char **interfaces;
	int num_interfaces;
	int recv_bufsize;
	int send_bufsize;
	int port;
	int new_sd;

	struct udp_listener_args *udp_args = listener->proto_private;

	if (listener->status != LISTENER_STOPPED)
		return -1;

	// grab the udp settings from the given policy
	if (psd_settings_get_int_2(listener->settings, "udp", "port", &port) != 0) {
		port = udp_def_incoming_port;
	}

	if (psd_settings_get_int_2(listener->settings, "udp", "recv_bufsize", &recv_bufsize) != 0) {
		recv_bufsize = udp_def_incoming_recv_bufsize;
	}

	if (psd_settings_get_int_2(listener->settings, "udp", "send_bufsize", &send_bufsize) != 0) {
		send_bufsize = udp_def_incoming_send_bufsize;
	}

	if (psd_settings_get_list_2(listener->settings, "udp", "interfaces", &interfaces, &num_interfaces) != 0) {
		interfaces = NULL;
	}

	if (psd_settings_get_bool_2(listener->settings, "udp", "use_ipv6", &use_ipv6) != 0) {
		use_ipv6 = udp_def_use_ipv6;
	}

	if (use_ipv6) {
		family = AF_INET6;
	} else {
		family = AF_INET;
	}
	
	new_sd = socket(family, SOCK_DGRAM, IPPROTO_UDP);
	if (new_sd < 0) {
		psd_err(0, "socket() failed");
		goto error_exit;
	}
	
	struct sockaddr_in si_me;	
	memset((char *) &si_me, 0, sizeof(si_me));
	si_me.sin_family = AF_INET;
	si_me.sin_port = htons(port);
	si_me.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(new_sd, &si_me, sizeof(si_me)) < 0){
		psd_err(0, "bind() failed");
		goto error_exit_sockets;
	}	

	udp_args->socket = new_sd;

	if (pthread_create(&udp_args->accept_thread, NULL, psd_udp_accept_handler, (void *) listener) != 0) {
		psd_err(0, "couldn't execute socket handler thread: %s", strerror(errno));
		goto error_exit_sockets;
	}

	listener->status = LISTENER_RUNNING;

	psd_info(0, "Started UDP listener: %s", listener->id);

	return 0;

error_exit_sockets:
	free(socket);
error_exit:
	return -1;
}

static int psd_proto_udp_stop_listener(psdListener *listener) {
	struct udp_listener_args *udp_args = listener->proto_private;
	int i, n;

	if (listener->status != LISTENER_RUNNING) {
		psd_err(0, "tried to stop a stopped listener: %d", listener->status);
		return -1;
	}

	if ((n = pthread_cancel(udp_args->accept_thread)) < 0){
		psd_err(0, "pthread_cancel failed: %d, %s", n, strerror(errno));
		return -1;
	}

	if ((n = pthread_join(udp_args->accept_thread, NULL)) < 0){
		psd_err(0, "pthread_join failed: %d, %s", n, strerror(errno));
		return -1;
	}

	close(udp_args->socket);
	
	listener->status = LISTENER_STOPPED;

	return 0;
}

static void psd_proto_udp_free_listener(psdListener *listener) {
	struct udp_listener_args *udp_args = listener->proto_private;

	if (listener->status != LISTENER_STOPPED)
		psd_proto_udp_stop_listener(listener);

	// once it is stopped, all the resources associated with the
	// listener(thread, sockets, etc) should be free, so we can just free
	// the struct
	free(udp_args);	
}

static void *psd_udp_accept_handler(void *v) {
	psdConn *new_conn;
	fd_set socket_set;
	int n, i, high;
	int sd;
	psdListener *listener;
	struct udp_listener_args *udp_args;
	int old_state;

	listener = v;
	udp_args = listener->proto_private;

	pthread_detach(pthread_self());

	new_conn = psd_conn_udp_alloc(udp_args->socket);
	if (!new_conn) {
		psd_err(1, "psd_conn_alloc_socket() failed: %s", strerror(errno));
		close(sd);
		return NULL;
	}
	
	new_conn->id = strdup(new_conn->description);
	
	psd_info(0, "new UDP psd_conn: %s", new_conn->description);
	
	new_conn->settings = psd_settings_duplicate(listener->settings);
	
	psd_info(0, "post settings duplicate: %s", new_conn->description);
	
	n = listener->callback(listener, new_conn, listener->arg);
	if (n == 0 && listener->one_shot)
		goto out;
	
	psd_info(0, "post call-back: %s", new_conn->description);
	
	// we did the callback with the UDP conn, now just loop until we're canceled
	while(1) {
		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_state);
		{	
			//maybe do some fancy stuff here
			sleep(10);
		}
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_state);
	}

	
out:
	psd_info(1, "Closing all sockets and exiting listener");
	
	close(udp_args->socket);
	
	listener->status = LISTENER_STOPPED;
	
	return NULL;
}
